package com.obixlabs.security.credentialsstore.server.dao.ldapimpl;

import static com.obixlabs.security.credentialsstore.server.dao.ldapimpl.LDAPConstants.COMMON_NAME_ATTRIBUTE;
import static com.obixlabs.security.credentialsstore.server.dao.ldapimpl.LDAPConstants.DESCRIPTION_ATTRIBUTE;
import static com.obixlabs.security.credentialsstore.server.dao.ldapimpl.LDAPConstants.OBJECT_CLASS_ATTRIBUTE;

import javax.naming.NameAlreadyBoundException;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.AttributeInUseException;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.NoSuchAttributeException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.obixlabs.commons.ExceptionUtil;
import com.obixlabs.security.credentialsstore.exception.EntryNotFoundException;
import com.obixlabs.security.credentialsstore.exception.SecurityRuntimeException;
import com.obixlabs.security.credentialsstore.model.ObixGroup;
import com.obixlabs.security.credentialsstore.model.ObixGroups;
import com.obixlabs.security.credentialsstore.model.ObixPermission;
import com.obixlabs.security.credentialsstore.model.ObixPermissions;
import com.obixlabs.security.credentialsstore.server.dao.PermissionDAO;

public class PermissionDAOImpl extends AbstractLDAPDAO implements PermissionDAO
{
	private static final Logger logger = LoggerFactory.getLogger(PermissionDAOImpl.class);
	
	private String GROUP_OF_UNIQUE_NAMES_CLASS_ATTR = "groupOfUniqueNames";	
	private String UNIQUE_MEMBER_ATTR_NAME = "uniqueMember";
	
	private String[] PERMISSION_OBJECT_CLASSES = {	"top",GROUP_OF_UNIQUE_NAMES_CLASS_ATTR};
		
	@Override
	public void delete(ObixPermission permission)
	{
		String permissionsDN =
			LDAPAttributesFactory.getPermissionDN(permission, 
													getPermissionCategorizationConfig());
		try 
		{
			getAdminConnection().getContext().destroySubcontext(permissionsDN);
		} 
		catch (NameNotFoundException exception) 
		{
			logger.warn("Permission (" + permissionsDN+ ") not found in directory. " +
						"Delete failed.", exception);
		} 
		catch (NamingException exception) 
		{
			throw new SecurityRuntimeException("Unexpected Error: Could not delete permission ("
								+ permissionsDN + ") from Obix directory.\n\tStack Trace:\n"
								+ ExceptionUtil.getStackTrace(exception));
		}
	}

	@Override
	public ObixPermissions findAll() { return findByGroup(null); }

	@Override
	public ObixPermissions findByGroup(ObixGroup group) 
	{
        ObixPermissions result = new ObixPermissions();
        
        try
        {
	        // Set up criteria to search on
	        StringBuffer filter = new StringBuffer();
        	
	        if (group!=null)
	        {
		        filter = filter	.append("(&")
								.append("(objectClass=" +
										GROUP_OF_UNIQUE_NAMES_CLASS_ATTR+")");
	        	
	        	filter = filter.append("("+UNIQUE_MEMBER_ATTR_NAME+"=")        				
	            				.append(LDAPAttributesFactory.getGroupDN(group, 
	            							getGroupCategorizationConfig()))
	            				.append(")");
	        	filter = filter.append(")");
	        }
	        else filter = filter.append("(objectClass=" + GROUP_OF_UNIQUE_NAMES_CLASS_ATTR+")");
	        	        	        
	        // Set up search constraints
	        SearchControls cons = new SearchControls();
	        cons.setSearchScope(SearchControls.ONELEVEL_SCOPE);
	
	        NamingEnumeration<SearchResult> results =
	            getAdminConnection().getContext().search(
	            			getCategorizationInfo().getPermissionCategorizationConfig().getPermissionsBaseDN(), 
	            			filter.toString(), cons);
	        
	        SearchResult searchResult;
	        ObixPermission permission;
	        while (results.hasMore()) 
	        {
	        	searchResult = results.next();
	        	permission = extractPermissionsInfo(searchResult);
	            result.put(permission.getName(), permission);
	        }
        }
        catch (NamingException exception)
        {
        	throw new SecurityRuntimeException(	"Could not search LDAP directory due " +
												"to unexpected exception.\n\tStack Trace:\n"+
												ExceptionUtil.getStackTrace(exception));        	
        }
        
        return result;
	}

	@Override
	public void save(ObixPermission permission) 
	{
		ObixPermission oldState = findByName(permission.getName());
		if (oldState == null)
			internalSaveNew(permission);
		else internalUpdateOld(oldState, permission);
	}
	
	@Override
	public ObixPermission findByName(String name) 
	{
        ObixPermission result;
        
        try
        {
	        // Set up criteria to search on
	        StringBuffer filter = new StringBuffer();

	        filter = filter	.append("(&")
							.append("(objectClass=" +
									GROUP_OF_UNIQUE_NAMES_CLASS_ATTR+")")
							.append("("+COMMON_NAME_ATTRIBUTE+"=")        				
							.append(name)
							.append(")")
							.append(")");
	        
	        	        	        	        
	        // Set up search constraints
	        SearchControls cons = new SearchControls();
	        cons.setSearchScope(SearchControls.ONELEVEL_SCOPE);
	
	        NamingEnumeration<SearchResult> results =
	            getAdminConnection().getContext().search(
	            			getCategorizationInfo().getPermissionCategorizationConfig().getPermissionsBaseDN(), 
	            			filter.toString(), cons);
	        
	        SearchResult searchResult;
	        if (results.hasMore()) 
	        {
	        	searchResult = results.next();
	        	result = extractPermissionsInfo(searchResult);	           
	        }
	        else result = null;
        }
        catch (NamingException exception)
        {
        	throw new SecurityRuntimeException(	"Could not search LDAP directory due " +
												"to unexpected exception.\n\tStack Trace:\n"+
												ExceptionUtil.getStackTrace(exception));        	
        }        
        return result;
	}	

	void assignPermission(ObixGroup group, ObixPermission permission) 
	{
		
		String permissionsDN = 
			LDAPAttributesFactory.getPermissionDN(permission, 
										getPermissionCategorizationConfig());

		String groupDN = LDAPAttributesFactory.getGroupDN(group, 
												getGroupCategorizationConfig());

		try 
		{
			ModificationItem[] modifications = new ModificationItem[1];

			Attribute permissionModification = new BasicAttribute(
					UNIQUE_MEMBER_ATTR_NAME, groupDN);

			modifications[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE,
					permissionModification);

			getAdminConnection().getContext().modifyAttributes(permissionsDN,
					modifications);
		} 
		catch (AttributeInUseException exception) 
		{
			
			String errorMessage = "Group (" + groupDN + ") is already assigned "
					+ "permission (" + permissionsDN + "). Request will be ignored.";
			logger.warn(errorMessage, exception);
		} 
		catch (NamingException exception) 
		{
			String errorMessage = "Unexpected Error: Could not assign permission ("
					+ permissionsDN + ") to group (" + groupDN
					+ "). Possibly already assigned." + "\n\tStack Trace:\n"
					+ ExceptionUtil.getStackTrace(exception);
			throw new SecurityRuntimeException(errorMessage);
		}
	}
	
	void revokePermission(ObixGroup group, ObixPermission permission)
	{
        String permissionsDN = 
        	LDAPAttributesFactory.getPermissionDN(permission,
        										getPermissionCategorizationConfig());

        String groupDN = LDAPAttributesFactory.getGroupDN(	group,
											getGroupCategorizationConfig());
 
		
        try 
        {
            ModificationItem[] modifications = new ModificationItem[1];

            Attribute permissionModification =
                new BasicAttribute(UNIQUE_MEMBER_ATTR_NAME,
                					groupDN);
            modifications[0] =
                new ModificationItem(	DirContext.REMOVE_ATTRIBUTE, 
                						permissionModification);
            
            getAdminConnection().getContext().modifyAttributes(	permissionsDN, 
            													modifications);
        } 
        catch (NoSuchAttributeException exception) 
        {
        	String errorMessage = 	"Group (" +groupDN+ ") is not assigned " +
									"permission (" + permissionsDN + ")" +
									". Request will be ignored!";
        	logger.warn(errorMessage,exception);        	        	
        }	
		catch (NamingException exception)
		{
        	String errorMessage = 	"Unexpected Error: Could not revoke permission (" +
        							permissionsDN+ ") from group (" + groupDN +
        							").\n\tStack Trace:\n"+
									ExceptionUtil.getStackTrace(exception);
        	throw new SecurityRuntimeException(errorMessage);  				
		}		
	}
	
	ObixGroups getGroupsAssignedTo(ObixPermission permission,
									GroupDAOImpl requester)
									throws EntryNotFoundException
	{
		ObixGroups result = new ObixGroups();
		try
		{
			String permissionsDN = 
				LDAPAttributesFactory.getPermissionDN(permission,
										getPermissionCategorizationConfig()); 
			Attributes permissionAttributes= 
				getAdminConnection().getContext().getAttributes(permissionsDN);
			
	        Attribute members = permissionAttributes.get(UNIQUE_MEMBER_ATTR_NAME);
	        NamingEnumeration<?> memberDNs=members.getAll();
	        
	        String groupDN;
	        ObixGroup group;
	        while(memberDNs.hasMore())
	        {
	        	groupDN = memberDNs.next().toString();
	        	if (!groupDN.equals(getAdminConnection().getPrincipalDN()))
	        	{
	        		group = requester.constructFromDN(groupDN,
	        										getAdminConnection());
	        		result.add(group);
	        	}
	        }			
		}
		catch (NameNotFoundException exce)
		{
			throw new EntryNotFoundException("Permission '" + permission + 
					"' does not exist in the directory. \n\tNamingException stack trace: " + 
					ExceptionUtil.getStackTrace(exce));		
		}
        catch (NamingException exce)
        {
        	throw new SecurityRuntimeException(	"Could not extract group members due " +
        										"to unexpected exception.\n\tStack Trace:\n"+
												ExceptionUtil.getStackTrace(exce));
        }
		return result;
	}


	private void internalUpdateOld(ObixPermission oldState, ObixPermission newState)
	{
		String permissionsDN = 
			LDAPAttributesFactory.getPermissionDN(oldState, 
										getPermissionCategorizationConfig());
		try 
		{
			ModificationItem[] modifications = new ModificationItem[1];

	        Attribute desc = new BasicAttribute(DESCRIPTION_ATTRIBUTE, 
												newState.getDescription());

			modifications[0] = new ModificationItem(
								DirContext.REPLACE_ATTRIBUTE, desc);

			getAdminConnection().getContext().modifyAttributes(
						permissionsDN, modifications);
		} 
		catch (NamingException exception) 
		{
			String errorMessage = "Unexpected Error: Could not update permission ("
					+ permissionsDN + "). Possible LDAP integrity error." + 
					"\n\tStack Trace:\n"
					+ ExceptionUtil.getStackTrace(exception);
			throw new SecurityRuntimeException(errorMessage);
		}		
	}	
	
	private void internalSaveNew(ObixPermission permission) 
	{
        // Create a container set of attributes
        Attributes container = new BasicAttributes();

        // Create the objectclass to add
        Attribute objClasses = 
        		new BasicAttribute(OBJECT_CLASS_ATTRIBUTE);
        for (String objectClass : PERMISSION_OBJECT_CLASSES)        	
        	objClasses.add(objectClass);


        // Assign the name and description to the group
        Attribute cn = new BasicAttribute(COMMON_NAME_ATTRIBUTE, 
        									permission.getName());
        Attribute desc = new BasicAttribute(DESCRIPTION_ATTRIBUTE, 
        									permission.getDescription());
                
        // Add these to the container
        container.put(objClasses);
        container.put(cn);
        container.put(desc);

        Attribute permissionAttr = 
        	new BasicAttribute(UNIQUE_MEMBER_ATTR_NAME,getAdminConnection().getPrincipalDN());                
        container.put(permissionAttr);

        LDAPAttributesFactory.addOrgIdentificationAttributes(	container, 
				getCategorizationInfo().getPermissionCategorizationConfig());
        
        
        // Create the entry        
        String permissionsDN = 
        	LDAPAttributesFactory.getPermissionDN(	permission,
        											getPermissionCategorizationConfig());
        
        try
        {
	        getAdminConnection().getContext().
	        			createSubcontext(permissionsDN, 
	        							container);
        }
        catch (NameAlreadyBoundException exception)
        {
        	throw new SecurityRuntimeException(	"Permission (" + permissionsDN + 
        		") already exists " +"in the Obix directory.\n\tStack Trace:\n"+
				ExceptionUtil.getStackTrace(exception));	
        }
		catch (NamingException exception)
		{			
        	throw new SecurityRuntimeException(	"Unexpected Error: Unable to add " +
        		"permission (" + permissionsDN +") to the Obix directory." +
        		"\n\tStack Trace:\n"+ExceptionUtil.getStackTrace(exception));				
		}
	}	
	
    private static ObixPermission extractPermissionsInfo(SearchResult searchResult) throws NamingException 
    {
		ObixPermission result = new ObixPermission();

		Attributes resultAttributes = searchResult.getAttributes();
		Attribute commonName = resultAttributes.get(COMMON_NAME_ATTRIBUTE);
		Attribute permissionDescription = resultAttributes
				.get(DESCRIPTION_ATTRIBUTE);

		result.setName(commonName.get().toString());
		result.setDescription(permissionDescription.get().toString());

		return result;
	}	
}
